package com.swingson.interpreter;

import com.swingson.SwingsonGui;
import com.swingson.exceptions.SwingsonException;
import com.swingson.interpreter.ast.*;
import com.swingson.interpreter.ast.base.BaseAst;
import com.swingson.interpreter.ast.base.BaseBinaryOperatorAst;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.swing.JComponent;

/**
 * @author sad
 */
public class Parser {

    public enum CalculationOrientation {

        horizontal, vertical, width, height;
    }

    private List<Token> parseStringToTokens(String string) {
        List<Token> tokens = new ArrayList<>();
        StringParserStream stream = new StringParserStream(string.toCharArray());
        Token token = stream.getToken();
        while (token != null) {
            tokens.add(token);
            token = stream.getToken();
        }
        return tokens;
    }

    public BaseAst parseString(JComponent currentComponent, JComponent previousComponent, SwingsonGui gui, String stringToParse, CalculationOrientation orientation) {
        try {
            List<Token> tokens = parseStringToTokens(stringToParse);
            PolishNotationParser parser = new PolishNotationParser();
            List<Token> polishTokens = parser.parseToPolishNotation(tokens);

            return convertToAst(polishTokens, currentComponent, previousComponent, gui, orientation);
        } catch (Exception ex) {
            throw new SwingsonException("Cannot parse string '" + stringToParse + "'");
        }
    }

    private BaseAst parseDigit(Token token) {
        String value = token.getValue();
        try {
            return new NumberAst(Float.parseFloat(value));
        } catch (NumberFormatException ex) {
            throw new SwingsonException("Digit should be a parseable float value [position:" + token.getStart() + "]");
        }
    }

    private BaseAst parseField(Token token, JComponent currentComponent, JComponent previousComponent, SwingsonGui gui) {
        String value = token.getValue();
        String[] strs = value.split("|");
        if (strs.length != 2) {
            throw new SwingsonException("Field value should contain two words separated with '.'(i.e. 'previous.width')");
        }

        JComponent operand = null;
        String object = strs[0];
        switch (object) {
            case "previous":
                operand = previousComponent;
                break;
            case "parent":
                operand = (JComponent) currentComponent.getParent();
                break;
            case "this":
                operand = currentComponent;
                break;
            case "top":
                operand = gui.getTopComponent();
                break;
            default:
                SwingsonGui.DefinedComponent def = gui.getDefinedComponent(value);
                if (def != null) {
                    operand = def.getComponent();
                } else {
                    throw new SwingsonException("Unrecognized component name " + value);
                }
                break;
        }

        String field = strs[1];
        switch (field) {
            case "width":
                return new GetWidthAst(operand);
            case "height":
                return new GetHeightAst(operand);
            case "x":
                return new GetXAst(operand);
            case "y":
                return new GetYAst(operand);
            default:
                throw new SwingsonException(String.format("Unrecognized field '%s' in component '%s'", field, object));
        }
    }
    private static final Map<String, Constructor> binaryOperatorToConstructors = new HashMap<String, Constructor>() {
        {
            try {
                put("+", AddAst.class.getConstructor());
                put("-", AddAst.class.getConstructor());
                put("*", AddAst.class.getConstructor());
                put("/", AddAst.class.getConstructor());
            } catch (NoSuchMethodException | SecurityException ex) {
                ex.printStackTrace();
            }
        }
    };

    private BaseAst parseOperator(Token token, JComponent currentComponent, JComponent previousComponent, SwingsonGui gui, int currentIndex, List tokens, CalculationOrientation orientation) throws Exception {
        String operator = token.getValue();
        if (binaryOperatorToConstructors.containsKey(operator)) {
            BaseBinaryOperatorAst ast = (BaseBinaryOperatorAst) binaryOperatorToConstructors.get(operator).newInstance();
            ast.setLeftOperand((BaseAst) tokens.get(currentIndex - 2));
            ast.setRightOperand((BaseAst) tokens.get(currentIndex - 1));
            tokens.set(currentIndex, ast);
            tokens.remove(currentIndex - 2);
            tokens.remove(currentIndex - 2);
            return ast;
        }
        if (operator.equals("%")) {
            BaseAst containerAst = null;
            switch (orientation) {
                case vertical:
                case height:
                    containerAst = new GetHeightAst((JComponent) currentComponent.getParent());
                    break;
                case horizontal:
                case width:
                    containerAst = new GetWidthAst((JComponent) currentComponent.getParent());
                    break;
            }

            BaseAst ast = new PercentAst((BaseAst) tokens.get(currentIndex - 1), containerAst);
            tokens.set(currentIndex, ast);
            tokens.remove(currentIndex - 1);
            return ast;
        }
        throw new RuntimeException("Cannot process token " + token.toString() + " as operator");
    }

    private BaseAst convertToAst(List<Token> polishNotation, JComponent currentComponent, JComponent previousComponent, SwingsonGui gui, CalculationOrientation orientation) throws Exception {
        List res = new LinkedList();
        res.addAll(polishNotation);
        while (true) {
            boolean processed = false;
            for (int i = 0; i < res.size(); i++) {
                Object obj = res.get(i);
                if (obj instanceof Token) {
                    processed = true;
                    Token t = (Token) obj;
                    if (t.getTokenType() == TokenType.digit) {
                        res.set(i, parseDigit(t));
                    } else if (t.getTokenType() == TokenType.field) {
                        res.set(i, parseField(t, currentComponent, previousComponent, gui));
                    } else {
                        parseOperator(t, currentComponent, previousComponent, gui, i, res, orientation);
                    }
                    break;
                }
            }
            if (processed == false) {
                break;
            }
        }
        return (BaseAst) res.get(0);
    }
}
